扩展 新增&编辑章节详情(富文本内容)
概述
课程详情页面中的章节管理需要支持新增和编辑富文本内容。由于富文本编辑器需要较大的展示空间,采用全页面表单替代右侧抽屉式弹窗,通过路由导航跳转到独立的编辑页面。新增和编辑共用同一个表单组件,通过路由参数区分操作模式。
页面路由设计
路由结构
// 路由配置
const routes = [
{
path: '/contents',
component: ContentLayout,
children: [
{
path: '',
name: 'content-list',
component: ContentList,
},
{
path: ':id',
name: 'content-detail',
component: ContentDetail,
},
{
path: ':id/add',
name: 'content-add',
component: ContentAddEditForm,
},
{
path: ':id/edit/:chapterId',
name: 'content-edit',
component: ContentAddEditForm,
},
],
},
]
ts
页面导航流程
内容列表 → 点击详情 → 章节详情页 → 点击新增 → 全页面表单
→ 点击编辑 → 全页面表单
关键决策:
- 富文本编辑需要充足空间 → 全页面表单
- 新增/编辑共用组件 → 通过 route.params.chapterId 区分
- 浏览器历史记录 → 使用 router.push 确保前进/后退可用
text
组件结构
components/contents/
├── ContentList.vue # 内容列表
├── ContentDetail.vue # 章节详情(含章节列表)
└── ContentAddEditForm.vue # 新增/编辑章节表单(全页面)
路由关系:
content-list → content-detail → content-add / content-edit
(列表页) (详情页) (全页面表单)
text
实现步骤
1. 创建编辑表单组件
<!-- components/contents/ContentAddEditForm.vue -->
<template>
<div class="chapter-form">
<div class="form-header">
<el-button @click="handleBack" :icon="ArrowLeft">返回</el-button>
<h3>{{ isEdit ? '编辑章节' : '新增章节' }}</h3>
</div>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="章节标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入章节标题" />
</el-form-item>
<el-form-item label="章节内容" prop="content">
<!-- 富文本编辑器占位 -->
<div class="editor-wrapper">
<RichTextEditor v-model="formData.content" />
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">保存</el-button>
<el-button @click="handleBack">取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus'
const route = useRoute()
const router = useRouter()
const formRef = ref<FormInstance>()
// 通过路由参数中是否存在 chapterId 来判断编辑/新增模式
const isEdit = computed(() => !!route.params.chapterId)
const formData = reactive({
title: '',
content: '',
})
const formRules = {
title: [{ required: true, message: '请输入章节标题', trigger: 'blur' }],
content: [{ required: true, message: '请输入章节内容', trigger: 'change' }],
}
// 编辑模式:加载已有数据
onMounted(async () => {
if (isEdit.value) {
// const res = await fetchChapterDetail(route.params.chapterId)
// Object.assign(formData, res.data)
}
})
const handleBack = () => {
router.back()
}
const handleSubmit = async () => {
await formRef.value?.validate()
// if (isEdit.value) {
// await updateChapter(route.params.chapterId, formData)
// } else {
// await createChapter(route.params.id, formData)
// }
router.back()
}
</script>
<style scoped>
.chapter-form {
padding: 20px;
}
.form-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.editor-wrapper {
width: 100%;
min-height: 400px;
}
</style>
vue
2. 详情页触发导航
<!-- ContentDetail.vue 关键部分 -->
<template>
<div class="content-detail">
<div class="action-bar">
<el-button type="primary" @click="handleAddChapter">
新增章节
</el-button>
</div>
<!-- 章节列表 -->
<div
v-for="chapter in chapters"
:key="chapter.id"
class="chapter-item"
>
<span>{{ chapter.title }}</span>
<el-button link @click="handleEditChapter(chapter.id)">
编辑
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const handleAddChapter = () => {
router.push({
name: 'content-add',
params: { id: route.params.id },
})
}
const handleEditChapter = (chapterId: string) => {
router.push({
name: 'content-edit',
params: {
id: route.params.id,
chapterId,
},
})
}
</script>
vue
为什么使用全页面而非抽屉
| 维度 | 右侧抽屉(Drawer) | 全页面表单 |
|---|---|---|
| 富文本空间 | 宽度受限,编辑区域过小 | 充分利用全屏空间 |
| 编辑体验 | 侧边操作不便 | 沉浸式编辑体验 |
| 布局复杂度 | 需处理遮罩和滚动穿透 | 独立页面,无冲突 |
| 路由管理 | 不产生浏览器历史记录 | 支持前进/后退导航 |
| 适用场景 | 简单表单、快速操作 | 复杂表单、富文本编辑 |
router.push 导航方式对比
// 方式一:字符串路径
router.push('/contents/123/add')
// 方式二:命名路由(推荐,路径变更时无需修改代码)
router.push({ name: 'content-add', params: { id: '123' } })
// 方式三:带查询参数
router.push({ name: 'content-add', params: { id: '123' }, query: { type: 'rich' } })
ts
推荐使用命名路由:当路由路径变更时,只需修改路由配置,无需逐一更新 router.push 调用。
富文本编辑器选型参考
| 编辑器 | 特点 | 适用场景 |
|---|---|---|
| Vditor | Markdown 所见即所得,支持多种模式 | 技术博客、课程内容 |
| TinyMCE | 功能全面的富文本编辑器 | 通用内容编辑 |
| WangEditor | 国产轻量级编辑器 | 简单表单 |
| Milkdown | 基于 ProseMirror 的插件化编辑器 | 需要高度定制 |
<!-- 集成 Vditor 示例 -->
<template>
<div ref="editorRef" class="vditor"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import Vditor from 'vditor'
import 'vditor/dist/index.css'
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
const editorRef = ref<HTMLElement>()
let vditor: Vditor | null = null
onMounted(() => {
vditor = new Vditor(editorRef.value!, {
height: 400,
mode: 'wysiwyg',
value: props.modelValue,
input: (value) => {
emit('update:modelValue', value)
},
})
})
watch(() => props.modelValue, (val) => {
if (vditor && vditor.getValue() !== val) {
vditor.setValue(val)
}
})
onBeforeUnmount(() => {
vditor?.destroy()
})
</script>
vue
实践要点
- 新增和编辑共用同一表单组件,通过
route.params中的chapterId区分模式 - 富文本编辑器需要较大的展示空间,应使用全页面布局而非侧边抽屉
- 使用
router.push({ name: '...' })命名路由导航,便于维护和路径管理 - 编辑页面需通过
router.back()提供返回功能,保持浏览器导航一致性 - 编辑模式下需要在
onMounted中加载已有章节数据填充表单 - 富文本编辑器的
v-model需要自定义实现,通过input事件和setValue方法双向同步
↑